package com.dbflow5.runtime;

import com.dbflow5.adapter.ModelAdapter;
import com.dbflow5.structure.ChangeAction;

import java.util.*;

/**
 * Description: Directly notifies about model changes. Users should use [.get] to use the shared
 * instance in [DatabaseConfig.Builder]
 */
public class DirectModelNotifier implements ModelNotifier {

    public static int instanceCount = 0;

    private DirectModelNotifier(){
        init();
    }

    private final LinkedHashMap<Class<?>, Set<OnModelStateChangedListener<?>>> modelChangedListenerMap = new LinkedHashMap<>();

    private final LinkedHashMap<Class<?>, Set<OnTableChangedListener>> tableChangedListenerMap = new LinkedHashMap<>();

    public interface OnModelStateChangedListener<T> {
        void onModelChanged(T model, ChangeAction action);
    }

    interface ModelChangedListener<T> extends OnModelStateChangedListener<T>, OnTableChangedListener{

    }

    private void init() {
        if (instanceCount > 0) {
            throw new IllegalStateException("Cannot instantiate more than one DirectNotifier. Use DirectNotifier.get()");
        }
        instanceCount++;
    }

    @Override
    public <T> void notifyModelChanged(T model, ModelAdapter<T> adapter, ChangeAction action) {
        Set<OnModelStateChangedListener<?>> values = modelChangedListenerMap.get(adapter.table());
        if(values != null) {
            for (OnModelStateChangedListener<?> listener : values){
                ((OnModelStateChangedListener<T>)listener).onModelChanged(model, action);
            }
        }

        Set<OnTableChangedListener> values2 = tableChangedListenerMap.get(adapter.table());
        if(values2 != null) {
            for(OnTableChangedListener listener : values2){
                listener.onTableChanged(adapter.table(), action);
            }
        }
    }

    @Override
    public <T> void notifyTableChanged(Class<T> table, ChangeAction action) {
        Set<OnTableChangedListener> values = tableChangedListenerMap.get(table);
        if(values != null) {
            for (OnTableChangedListener listener : values){
                listener.onTableChanged(table, action);
            }
        }
    }

    @Override
    public TableNotifierRegister newRegister() {
        return new DirectTableNotifierRegister(this);
    }

    public <T> void registerForModelChanges(Class<T> table, ModelChangedListener<T> listener) {
        registerForModelStateChanges(table, listener);
        registerForTableChanges(table, listener);
    }

    public <T> void registerForModelStateChanges(Class<T> table, OnModelStateChangedListener<T> listener) {
        Set<OnModelStateChangedListener<?>> listeners = modelChangedListenerMap.get(table);
        if (listeners == null) {
            listeners = new LinkedHashSet<>();
            modelChangedListenerMap.put(table, listeners);
        }
        listeners.add(listener);
    }

    public  <T> void registerForTableChanges(Class<T> table, OnTableChangedListener listener) {
        Set<OnTableChangedListener> listeners = tableChangedListenerMap.get(table);
        if (listeners == null) {
            listeners = new LinkedHashSet<>();
            tableChangedListenerMap.put(table, listeners);
        }
        listeners.add(listener);
    }

    public <T> void unregisterForModelChanges(Class<T> table, ModelChangedListener<T> listener) {
        unregisterForModelStateChanges(table, listener);
        unregisterForTableChanges(table, listener);
    }


    public <T> void unregisterForModelStateChanges(Class<T> table, OnModelStateChangedListener<T> listener) {
        Set<OnModelStateChangedListener<?>> listeners = modelChangedListenerMap.get(table);
        listeners.remove(listener);
    }

    public <T> void unregisterForTableChanges(Class<T> table, OnTableChangedListener listener) {
        Set<OnTableChangedListener> listeners = tableChangedListenerMap.get(table);
        listeners.remove(listener);
    }

    /**
     * Clears all listeners.
     */
    public void clearListeners(){
        tableChangedListenerMap.clear();
    }

    private static class DirectTableNotifierRegister implements TableNotifierRegister {
        private final DirectModelNotifier directModelNotifier;

        public DirectTableNotifierRegister(DirectModelNotifier directModelNotifier){
            this.directModelNotifier = directModelNotifier;
        }

        private final List<Class<?>> registeredTables = new ArrayList<>();

        private OnTableChangedListener modelChangedListener = null;

        private final OnTableChangedListener internalChangeListener = new OnTableChangedListener() {
            @Override
            public void onTableChanged(Class<?> table, ChangeAction action) {
                modelChangedListener.onTableChanged(table, action);
            }
        };

        @Override
        public boolean isSubscribed() {
            return !registeredTables.isEmpty();
        }

        @Override
        public <T> void register(Class<T> tClass) {
            registeredTables.add(tClass);
            directModelNotifier.registerForTableChanges(tClass, internalChangeListener);
        }

        @Override
        public <T> void unregister(Class<T> tClass) {
            registeredTables.remove(tClass);
            directModelNotifier.unregisterForTableChanges(tClass, internalChangeListener);
        }

        @Override
        public void unregisterAll() {
            for(Class<?> table : registeredTables){
                directModelNotifier.unregisterForTableChanges(table, internalChangeListener);
            }
            this.modelChangedListener = null;
        }

        @Override
        public void setListener(OnTableChangedListener listener) {
            this.modelChangedListener = listener;
        }
    }

    private static final DirectModelNotifier notifier = new DirectModelNotifier();

    public static DirectModelNotifier get() {
        return notifier;
    }

    public static DirectModelNotifier invoke() {
        return get();
    }
}
